/*
 * Copyright (c) 2005-2009 Flamingo Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Flamingo Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package common.ui;

import java.applet.Applet;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.plaf.ActionMapUIResource;
import javax.swing.plaf.BorderUIResource;
import javax.swing.plaf.ComponentInputMapUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.ComboPopup;

import utils.FlamingoUtilities;

import common.JCommandButton;
import common.popup.JPopupPanel;
import common.popup.PopupPanelManager;
import common.popup.PopupPanelManager.PopupEvent;

/**
 * Basic UI for popup panel {@link JPopupPanel}.
 * 
 * @author Kirill Grouchnikov
 */
public class BasicPopupPanelUI extends PopupPanelUI {
    /**
     * The associated popup panel.
     */
    protected JPopupPanel popupPanel;

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
     */
    public static ComponentUI createUI(JComponent c) {
        return new BasicPopupPanelUI();
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.plaf.ComponentUI#installUI(javax.swing.JComponent)
     */
    @Override
    public void installUI(JComponent c) {
        this.popupPanel = (JPopupPanel) c;
        super.installUI(this.popupPanel);
        installDefaults();
        installComponents();
        installListeners();
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.plaf.ComponentUI#uninstallUI(javax.swing.JComponent)
     */
    @Override
    public void uninstallUI(JComponent c) {
        uninstallListeners();
        uninstallComponents();
        uninstallDefaults();
        super.uninstallUI(this.popupPanel);
    }

    /**
     * Installs default settings for the associated command popup menu.
     */
    protected void installDefaults() {
        Color bg = this.popupPanel.getBackground();
        if (bg == null || bg instanceof UIResource) {
            this.popupPanel.setBackground(FlamingoUtilities.getColor(
                    Color.lightGray, "PopupPanel.background",
                    "Panel.background"));
        }

        Border b = this.popupPanel.getBorder();
        if (b == null || b instanceof UIResource) {
            Border toSet = UIManager.getBorder("PopupPanel.border");
            if (toSet == null)
                toSet = new BorderUIResource.CompoundBorderUIResource(
                        new LineBorder(FlamingoUtilities.getBorderColor()),
                        new EmptyBorder(1, 1, 1, 1));
            this.popupPanel.setBorder(toSet);
        }
        LookAndFeel.installProperty(this.popupPanel, "opaque", Boolean.TRUE);
    }

    /**
     * Installs listeners on the associated command popup menu.
     */
    protected void installListeners() {
        initiliazeGlobalListeners();
    }

    /**
     * Installs components on the associated command popup menu.
     */
    protected void installComponents() {
    }

    /**
     * Uninstalls default settings from the associated command popup menu.
     */
    protected void uninstallDefaults() {
        LookAndFeel.uninstallBorder(this.popupPanel);
    }

    /**
     * Uninstalls listeners from the associated command popup menu.
     */
    protected void uninstallListeners() {
    }

    /**
     * Uninstalls subcomponents from the associated command popup menu.
     */
    protected void uninstallComponents() {
    }

    /**
     * The global listener that tracks the ESC key action on the root panes of
     * windows that show popup panels.
     */
    static PopupPanelManager.PopupListener popupPanelManagerListener;

    /**
     * Initializes the global listeners.
     */
    protected static synchronized void initiliazeGlobalListeners() {
        if (popupPanelManagerListener != null) {
            return;
        }

        popupPanelManagerListener = new PopupPanelEscapeDismisser();
        PopupPanelManager.defaultManager().addPopupListener(
                popupPanelManagerListener);

        new WindowTracker();
    }

    /**
     * This class is used to trace the changes in the shown popup panels and
     * install ESC key listener on the matching root pane so that the popup
     * panels can be dismissed with the ESC key.
     * 
     * @author Kirill Grouchnikov
     */
    protected static class PopupPanelEscapeDismisser implements
            PopupPanelManager.PopupListener {
        /**
         * The currently installed action map on the {@link #tracedRootPane}.
         */
        private ActionMap newActionMap;

        /**
         * The currently installed input map on the {@link #tracedRootPane}.
         */
        private InputMap newInputMap;

        /**
         * The last shown popup panel sequence.
         */
        List<PopupPanelManager.PopupInfo> lastPathSelected;

        /**
         * Currently traced root pane. It is the root pane of the originating
         * component of the first popup panel in the currently shown sequence of
         * {@link PopupPanelManager}.
         */
        private JRootPane tracedRootPane;

        /**
         * Creates a new tracer for popup panels to be dismissed with ESC key.
         */
        public PopupPanelEscapeDismisser() {
            PopupPanelManager popupPanelManager = PopupPanelManager
                    .defaultManager();
            this.lastPathSelected = popupPanelManager.getShownPath();
            if (this.lastPathSelected.size() != 0) {
                traceRootPane(this.lastPathSelected);
            }
        }

        @Override
        public void popupHidden(PopupEvent event) {
            PopupPanelManager msm = PopupPanelManager.defaultManager();
            List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

            if (lastPathSelected.size() != 0 && p.size() == 0) {
                // if it is the last popup panel to be dismissed, untrace the
                // root pane
                untraceRootPane();
            }

            lastPathSelected = p;
        }

        /**
         * Removes the installed maps on the currently traced root pane.
         */
        private void untraceRootPane() {
            if (this.tracedRootPane != null) {
                removeUIActionMap(this.tracedRootPane, this.newActionMap);
                removeUIInputMap(this.tracedRootPane, this.newInputMap);
            }
        }

        @Override
        public void popupShown(PopupEvent event) {
            PopupPanelManager msm = PopupPanelManager.defaultManager();
            List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

            if (lastPathSelected.size() == 0 && p.size() != 0) {
                // if it is the first popup panel to be shown, trace the root
                // panel
                traceRootPane(p);
            }

            lastPathSelected = p;
        }

        /**
         * Installs the maps on the root pane of the originating component of
         * the first popup panel of the specified sequence to trace the ESC key
         * and dismiss the shown popup panels.
         * 
         * @param shownPath
         *            Popup panel sequence.
         */
        private void traceRootPane(List<PopupPanelManager.PopupInfo> shownPath) {
            JComponent originator = shownPath.get(0).getPopupOriginator();
            this.tracedRootPane = SwingUtilities.getRootPane(originator);

            if (this.tracedRootPane != null) {
                newInputMap = new ComponentInputMapUIResource(tracedRootPane);
                newInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
                        "hidePopupPanel");

                newActionMap = new ActionMapUIResource();
                newActionMap.put("hidePopupPanel", new AbstractAction() {
                    /**
                     * 
                     */
                    private static final long serialVersionUID = 1L;

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        PopupPanelManager.defaultManager().hideLastPopup();
                    }
                });

                addUIInputMap(tracedRootPane, newInputMap);
                addUIActionMap(tracedRootPane, newActionMap);
            }
        }

        /**
         * Adds the specified input map to the specified component.
         * 
         * @param c
         *            Component.
         * @param map
         *            Input map to add.
         */
        void addUIInputMap(JComponent c, InputMap map) {
            InputMap lastNonUI = null;
            InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);

            while (parent != null && !(parent instanceof UIResource)) {
                lastNonUI = parent;
                parent = parent.getParent();
            }

            if (lastNonUI == null) {
                c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map);
            } else {
                lastNonUI.setParent(map);
            }
            map.setParent(parent);
        }

        /**
         * Adds the specified action map to the specified component.
         * 
         * @param c
         *            Component.
         * @param map
         *            Action map to add.
         */
        void addUIActionMap(JComponent c, ActionMap map) {
            ActionMap lastNonUI = null;
            ActionMap parent = c.getActionMap();

            while (parent != null && !(parent instanceof UIResource)) {
                lastNonUI = parent;
                parent = parent.getParent();
            }

            if (lastNonUI == null) {
                c.setActionMap(map);
            } else {
                lastNonUI.setParent(map);
            }
            map.setParent(parent);
        }

        /**
         * Removes the specified input map from the specified component.
         * 
         * @param c
         *            Component.
         * @param map
         *            Input map to remove.
         */
        void removeUIInputMap(JComponent c, InputMap map) {
            InputMap im = null;
            InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);

            while (parent != null) {
                if (parent == map) {
                    if (im == null) {
                        c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map
                                .getParent());
                    } else {
                        im.setParent(map.getParent());
                    }
                    break;
                }
                im = parent;
                parent = parent.getParent();
            }
        }

        /**
         * Removes the specified action map from the specified component.
         * 
         * @param c
         *            Component.
         * @param map
         *            Action map to remove.
         */
        void removeUIActionMap(JComponent c, ActionMap map) {
            ActionMap im = null;
            ActionMap parent = c.getActionMap();

            while (parent != null) {
                if (parent == map) {
                    if (im == null) {
                        c.setActionMap(map.getParent());
                    } else {
                        im.setParent(map.getParent());
                    }
                    break;
                }
                im = parent;
                parent = parent.getParent();
            }
        }
    }

    /**
     * This class is used to dismiss popup panels on the following events:
     * 
     * <ul>
     * <li>Mouse click outside any shown popup panel.</li>
     * <li>Closing, iconifying or deactivation of a top-level window.</li>
     * <li>Any change in the component hierarchy of a top-level window.</li>
     * </ul>
     * 
     * Only one top-level window is tracked at any time. The assumption is that
     * the {@link PopupPanelManager} only shows popup panels originating from
     * one top-level window.
     * 
     * @author Kirill Grouchnikov
     */
    protected static class WindowTracker implements
            PopupPanelManager.PopupListener, AWTEventListener,
            ComponentListener, WindowListener {

        /**
         * The currently tracked window. It is the window of the originating
         * component of the first popup panel in the currently shown sequence of
         * {@link PopupPanelManager}.
         */
        Window grabbedWindow;

        /**
         * Last selected path in the {@link PopupPanelManager}.
         */
        List<PopupPanelManager.PopupInfo> lastPathSelected;

        /**
         * Creates the new window tracker.
         */
        public WindowTracker() {
            PopupPanelManager popupPanelManager = PopupPanelManager
                    .defaultManager();
            popupPanelManager.addPopupListener(this);
            this.lastPathSelected = popupPanelManager.getShownPath();
            if (this.lastPathSelected.size() != 0) {
                grabWindow(this.lastPathSelected);
            }
        }

        /**
         * Grabs the window of the first popup panel in the specified popup
         * panel sequence.
         * 
         * @param shownPath
         *            Sequence of the currently shown popup panels.
         */
        @SuppressWarnings("unchecked")
        void grabWindow(List<PopupPanelManager.PopupInfo> shownPath) {
            final Toolkit tk = Toolkit.getDefaultToolkit();
            java.security.AccessController
                    .doPrivileged(new java.security.PrivilegedAction() {
                        public Object run() {
                            tk.addAWTEventListener(WindowTracker.this,
                                    AWTEvent.MOUSE_EVENT_MASK
                                            | AWTEvent.MOUSE_MOTION_EVENT_MASK
                                            | AWTEvent.MOUSE_WHEEL_EVENT_MASK
                                            | AWTEvent.WINDOW_EVENT_MASK);
                            return null;
                        }
                    });

            Component invoker = shownPath.get(0).getPopupOriginator();
            grabbedWindow = invoker instanceof Window ? (Window) invoker
                    : SwingUtilities.getWindowAncestor(invoker);
            if (grabbedWindow != null) {
                grabbedWindow.addComponentListener(this);
                grabbedWindow.addWindowListener(this);
            }
        }

        /**
         * Ungrabs the currently tracked window.
         */
        @SuppressWarnings("unchecked")
        void ungrabWindow() {
            final Toolkit tk = Toolkit.getDefaultToolkit();
            // The grab should be removed
            java.security.AccessController
                    .doPrivileged(new java.security.PrivilegedAction() {
                        public Object run() {
                            tk.removeAWTEventListener(WindowTracker.this);
                            return null;
                        }
                    });
            if (grabbedWindow != null) {
                grabbedWindow.removeComponentListener(this);
                grabbedWindow.removeWindowListener(this);
                grabbedWindow = null;
            }
        }

        @Override
        public void popupShown(PopupEvent event) {
            PopupPanelManager msm = PopupPanelManager.defaultManager();
            List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

            if (lastPathSelected.size() == 0 && p.size() != 0) {
                // if it is the first popup panel to be shown, grab its window
                grabWindow(p);
            }

            lastPathSelected = p;
        }

        @Override
        public void popupHidden(PopupEvent event) {
            PopupPanelManager msm = PopupPanelManager.defaultManager();
            List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

            if (lastPathSelected.size() != 0 && p.size() == 0) {
                // if it is the last popup panel to be hidden, ungrab its window
                ungrabWindow();
            }

            lastPathSelected = p;
        }

        public void eventDispatched(AWTEvent ev) {
            if (!(ev instanceof MouseEvent)) {
                // We are interested in MouseEvents only
                return;
            }
            MouseEvent me = (MouseEvent) ev;
            final Component src = me.getComponent();
            JPopupPanel popupPanelParent = (JPopupPanel) SwingUtilities
                    .getAncestorOfClass(JPopupPanel.class, src);
            switch (me.getID()) {
            case MouseEvent.MOUSE_PRESSED:
                boolean wasCommandButtonPopupShowing = false;
                if (src instanceof JCommandButton) {
                    wasCommandButtonPopupShowing = ((JCommandButton) src)
                            .getPopupModel().isPopupShowing();
                }

                if (!wasCommandButtonPopupShowing && (popupPanelParent != null)) {
                    // close all popups until this parent and return
                    PopupPanelManager.defaultManager().hidePopups(
                            popupPanelParent);
                    return;
                }
//                if (src instanceof JRibbonTaskToggleButton) {
//                    JRibbon ribbon = (JRibbon) SwingUtilities
//                            .getAncestorOfClass(JRibbon.class, src);
//                    if ((ribbon != null)
//                            && FlamingoUtilities
//                                    .isShowingMinimizedRibbonInPopup(ribbon)) {
//                        // This will be handled in the action listener installed
//                        // on ribbon task toggle buttons in BasicRibbonUI.
//                        // There the ribbon popup will be hidden.
//                        return;
//                    }
//                }

                // if the popup of command button was showing, it will be hidden
                // in BasicCommandButtonUI.processPopupAction() - via
                // BasicCommandButtonUI.createPopupActionListener().
                if (!wasCommandButtonPopupShowing) {
                    // special case - ignore mouse press on an item in a combo
                    // popup
                    if (SwingUtilities
                            .getAncestorOfClass(ComboPopup.class, src) == null) {
                        PopupPanelManager.defaultManager().hidePopups(src);
                    }
                }

                // pass the event so that it gets processed by the controls
                break;

            case MouseEvent.MOUSE_RELEASED:
                // special case - mouse release on an item in a combo popup
                if (SwingUtilities.getAncestorOfClass(ComboPopup.class, src) != null) {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            PopupPanelManager.defaultManager().hidePopups(src);
                        }
                    });
                }

                // pass the event so that it gets processed by the controls
                break;

            case MouseEvent.MOUSE_WHEEL:
                if (popupPanelParent != null) {
                    // close all popups until this parent and return
                    PopupPanelManager.defaultManager().hidePopups(
                            popupPanelParent);
                    return;
                }

                PopupPanelManager.defaultManager().hidePopups(src);
                break;
            }
        }

        /**
         * Checks whether the specified component lies inside a
         * {@link JPopupPanel}.
         * 
         * @param src
         *            Component.
         * @return <code>true</code> if the specified component lies inside a
         *         {@link JPopupPanel}.
         */
        boolean isInPopupPanel(Component src) {
            for (Component c = src; c != null; c = c.getParent()) {
                if (c instanceof Applet || c instanceof Window) {
                    break;
                } else if (c instanceof JPopupPanel) {
                    return true;
                }
            }
            return false;
        }

        public void componentResized(ComponentEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void componentMoved(ComponentEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void componentShown(ComponentEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void componentHidden(ComponentEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void windowClosing(WindowEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void windowClosed(WindowEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void windowIconified(WindowEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void windowDeactivated(WindowEvent e) {
            PopupPanelManager.defaultManager().hidePopups(null);
        }

        public void windowOpened(WindowEvent e) {
        }

        public void windowDeiconified(WindowEvent e) {
        }

        public void windowActivated(WindowEvent e) {
        }
    }
}
